import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';

import { IgnoreFunction } from '@electron/packager';
import { ResolvedForgeConfig } from '@electron-forge/shared-types';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';

import {
  WebpackPluginConfig,
  WebpackPluginRendererConfig,
} from '../src/Config';
import { WebpackPlugin } from '../src/WebpackPlugin';

describe('WebpackPlugin', async () => {
  const baseConfig: WebpackPluginConfig = {
    mainConfig: {},
    renderer: {
      config: {},
      entryPoints: [],
    },
  };

  const tmp = os.tmpdir();
  const tmpdir = path.join(tmp, 'electron-forge-test-');
  const webpackTestDir = await fs.promises.mkdtemp(tmpdir);

  afterAll(async () => {
    await fs.promises.rm(webpackTestDir, { recursive: true });
  });

  describe('TCP port', () => {
    it('should fail for privileged ports', () => {
      expect(
        () => new WebpackPlugin({ ...baseConfig, loggerPort: 80 }),
      ).toThrow(/privileged$/);
    });

    it('should fail for too-large port numbers', () => {
      expect(
        () => new WebpackPlugin({ ...baseConfig, loggerPort: 99999 }),
      ).toThrow(/not a valid TCP port/);
    });
  });

  describe('packageAfterCopy', () => {
    const packageJSONPath = path.join(webpackTestDir, 'package.json');
    const packagedPath = path.join(webpackTestDir, 'packaged');
    const packagedPackageJSONPath = path.join(packagedPath, 'package.json');
    let plugin: WebpackPlugin;

    beforeAll(async () => {
      await fs.promises.mkdir(packagedPath);
      plugin = new WebpackPlugin(baseConfig);
      plugin.setDirectories(webpackTestDir);
    });

    it('should remove config.forge from package.json', async () => {
      const packageJSON = {
        main: './.webpack/main',
        config: { forge: 'config.js' },
      };
      await fs.promises.writeFile(
        packageJSONPath,
        JSON.stringify(packageJSON),
        'utf-8',
      );
      await plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath);
      expect(fs.existsSync(packagedPackageJSONPath)).toEqual(true);
      expect(
        JSON.parse(await fs.promises.readFile(packagedPackageJSONPath, 'utf-8'))
          .config,
      ).not.toHaveProperty('forge');
    });

    it('should succeed if there is no config.forge', async () => {
      const packageJSON = { main: '.webpack/main' };
      await fs.promises.writeFile(
        packageJSONPath,
        JSON.stringify(packageJSON),
        'utf-8',
      );
      await plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath);
      expect(fs.existsSync(packagedPackageJSONPath)).toEqual(true);
      expect(
        JSON.parse(
          await fs.promises.readFile(packagedPackageJSONPath, 'utf-8'),
        ),
      ).not.toHaveProperty('config');
    });

    it('should fail if there is no main key in package.json', async () => {
      const packageJSON = {};
      await fs.promises.writeFile(
        packageJSONPath,
        JSON.stringify(packageJSON),
        'utf-8',
      );
      await expect(
        plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath),
      ).rejects.toThrow(/entry point/);
    });

    it('should fail if main in package.json does not end with .webpack/main', async () => {
      const packageJSON = { main: 'src/main.js' };
      await fs.promises.writeFile(
        packageJSONPath,
        JSON.stringify(packageJSON),
        'utf-8',
      );
      await expect(
        plugin.packageAfterCopy({} as ResolvedForgeConfig, packagedPath),
      ).rejects.toThrow(/entry point/);
    });
  });

  describe('resolveForgeConfig', () => {
    let plugin: WebpackPlugin;

    beforeAll(() => {
      plugin = new WebpackPlugin(baseConfig);
    });

    it('sets packagerConfig and packagerConfig.ignore if it does not exist', async () => {
      const config = await plugin.resolveForgeConfig({} as ResolvedForgeConfig);
      expect(config.packagerConfig).not.toEqual(undefined);
      expect(config.packagerConfig.ignore).toBeTypeOf('function');
    });

    describe('packagerConfig.ignore', () => {
      it('does not overwrite an existing ignore value', async () => {
        const config = await plugin.resolveForgeConfig({
          packagerConfig: {
            ignore: /test/,
          },
        } as ResolvedForgeConfig);

        expect(config.packagerConfig.ignore).toEqual(/test/);
      });

      it('ignores everything but files in .webpack', async () => {
        const config = await plugin.resolveForgeConfig(
          {} as ResolvedForgeConfig,
        );
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(ignore('')).toEqual(false);
        expect(ignore('/abc')).toEqual(true);
        expect(ignore('/.webpack')).toEqual(false);
        expect(ignore('/.webpack/foo')).toEqual(false);
      });

      it('ignores files generated by jsonStats config', async () => {
        const webpackConfig = { ...baseConfig, jsonStats: true };
        (webpackConfig.renderer as WebpackPluginRendererConfig).jsonStats =
          true;
        plugin = new WebpackPlugin(webpackConfig);
        const config = await plugin.resolveForgeConfig(
          {} as ResolvedForgeConfig,
        );
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(
          ignore(path.join('foo', 'bar', '.webpack', 'main', 'stats.json')),
        ).toEqual(true);
        expect(
          ignore(path.join('foo', 'bar', '.webpack', 'renderer', 'stats.json')),
        ).toEqual(true);
      });

      it('ignores source map files by default', async () => {
        const webpackConfig = { ...baseConfig };
        plugin = new WebpackPlugin(webpackConfig);
        const config = await plugin.resolveForgeConfig(
          {} as ResolvedForgeConfig,
        );
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(ignore(path.join('/.webpack', 'main', 'index.js'))).toEqual(
          false,
        );
        expect(ignore(path.join('/.webpack', 'main', 'index.js.map'))).toEqual(
          true,
        );
        expect(
          ignore(path.join('/.webpack', 'renderer', 'main_window', 'index.js')),
        ).toEqual(false);
        expect(
          ignore(
            path.join('/.webpack', 'renderer', 'main_window', 'index.js.map'),
          ),
        ).toEqual(true);
      });

      it('includes source map files when specified by config', async () => {
        const webpackConfig = { ...baseConfig, packageSourceMaps: true };
        plugin = new WebpackPlugin(webpackConfig);
        const config = await plugin.resolveForgeConfig(
          {} as ResolvedForgeConfig,
        );
        const ignore = config.packagerConfig.ignore as IgnoreFunction;

        expect(ignore(path.join('/.webpack', 'main', 'index.js'))).toEqual(
          false,
        );
        expect(ignore(path.join('/.webpack', 'main', 'index.js.map'))).toEqual(
          false,
        );
        expect(
          ignore(path.join('/.webpack', 'renderer', 'main_window', 'index.js')),
        ).toEqual(false);
        expect(
          ignore(
            path.join('/.webpack', 'renderer', 'main_window', 'index.js.map'),
          ),
        ).toEqual(false);
      });
    });
  });

  describe('devServerOptions', () => {
    it('can override defaults', () => {
      const defaultPlugin = new WebpackPlugin(baseConfig);
      defaultPlugin.setDirectories(webpackTestDir);
      expect(defaultPlugin.devServerOptions().hot).toEqual(true);

      const webpackConfig = {
        ...baseConfig,
        devServer: {
          hot: false,
        },
      };

      const plugin = new WebpackPlugin(webpackConfig);
      plugin.setDirectories(webpackTestDir);
      const devServerConfig = plugin.devServerOptions();

      expect(devServerConfig.hot).toEqual(false);
    });

    it('cannot override certain configuration', () => {
      const webpackConfig = {
        ...baseConfig,
        port: 9999,
        devServer: {
          port: 8888,
          headers: {
            'Content-Security-Policy': 'invalid',
            'X-Test-Header': 'test',
          },
        },
      };

      const plugin = new WebpackPlugin(webpackConfig);
      plugin.setDirectories(webpackTestDir);
      const devServerConfig = plugin.devServerOptions();

      expect(devServerConfig.port).toEqual(9999);
      const headers = devServerConfig.headers as Record<string, string>;
      expect(headers['Content-Security-Policy']).not.toEqual('invalid');
      expect(headers['X-Test-Header']).toEqual('test');
    });
  });
});
